# Copyright (c) 2017 Presto Labs Pte. Ltd.
# Author: inkyu

from collections import namedtuple
import datetime

from recordclass import recordclass

from coin.base.book.price_map import BidPriceMap, AskPriceMap
from coin.base.book.observable_price_map import (PriceMapObserverBase,
                                                 ObservableBidPriceMap,
                                                 ObservableAskPriceMap)
from coin.exchange.base.kr_rest.product import NativeProduct
from coin.exchange.base.kr_rest.futures_product import NativeFuturesProduct

PriceQty = namedtuple('PriceQty', ['price', 'qty'])
MutablePriceQty = recordclass('MutablePriceQty', ['price', 'qty'])


class ThinBookError(Exception):
  pass


class BookBuilderObserverBase:
  def on_ask_clear(self, book):
    pass

  def on_ask_update(self, book, price, entry):
    pass

  def on_bid_clear(self, book):
    pass

  def on_bid_update(self, book, price, entry):
    pass


class BookBuilderBase:
  ProductType = None

  def __init__(self, exchange, native_symbol, current_datetime=None, book_builder_observer=None):
    self.book_builder_create_datetime = current_datetime or datetime.datetime.now()

    self.request_timestamp = None
    self.fetched_timestamp = None
    self.exchange_timestamp = None
    self.topic_id = None
    self.topic_str = None

    self.exchange = exchange
    if self.ProductType is not None:
      self.product = self.FromStrNativeProduct(native_symbol, current_datetime)
      self.native_symbol = self.product.native_symbol
      self.norm_symbol = self.product.symbol
      # for backward compatibility, book's symbol is native symbol
      self.symbol = native_symbol
    else:
      self.product = None
      self.native_symbol = native_symbol
      self.norm_symbol = native_symbol
      self.symbol = native_symbol

    self._book_builder_observer = book_builder_observer
    self._ask_price_map = None
    self._bid_price_map = None
    self._reset_price_map()

  def set_observer(self, observer):
    if self._book_builder_observer is observer:
      return

    if observer is None:
      self.reset_observer()
      return

    assert self._book_builder_observer is None
    self._book_builder_observer = observer
    self._ask_price_map = ObservableAskPriceMap(_AskPriceMapObserver(self, observer),
                                                self._ask_price_map)
    self._bid_price_map = ObservableBidPriceMap(_BidPriceMapObserver(self, observer),
                                                self._bid_price_map)
    self._ask_price_map.send_snapshot_to_observer()
    self._bid_price_map.send_snapshot_to_observer()

  def reset_observer(self):
    if self._book_builder_observer is None:
      return

    self._ask_price_map = self._ask_price_map.underlying_price_map
    self._bid_price_map = self._bid_price_map.underlying_price_map
    self._book_builder_observer = None

  def _reset_price_map(self):
    if self._book_builder_observer is None:
      self._ask_price_map = AskPriceMap()
      self._bid_price_map = BidPriceMap()
    else:
      observer, self._book_builder_observer = self._book_builder_observer, None
      self._ask_price_map = None
      self._bid_price_map = None
      self.set_observer(observer)

  def FromStrNativeProduct(self, native_symbol, current_datetime=None):
    if self.ProductType is not None:
      if issubclass(self.ProductType, NativeProduct):
        return self.ProductType.FromStrNativeProduct(native_symbol)
      elif issubclass(self.ProductType, NativeFuturesProduct):
        current_datetime = current_datetime or self.book_builder_create_datetime
        return self.ProductType.FromStrNativeProduct(native_symbol, current_datetime)
      else:
        raise TypeError(self.ProductType)
    else:
      return None

  @property
  def timestamp(self):
    return self.fetched_timestamp

  def update_timestamp(self, timestamp):
    self.fetched_timestamp = timestamp

  def update_exchange_timestamp(self, timestamp):
    self.exchange_timestamp = timestamp

  def update_topic_id(self, topic_id):
    self.topic_id = topic_id

  def update_topic_str(self, topic_str):
    self.topic_str = topic_str

  def clear_book(self):
    self._bid_price_map.clear()
    self._ask_price_map.clear()

  def has_bid(self):
    return self._bid_price_map.not_empty()

  def has_ask(self):
    return self._ask_price_map.not_empty()

  def bid0(self):
    return self._bid_price_map.get_min()

  def ask0(self):
    return self._ask_price_map.get_min()

  def iter_ask(self):
    for price, elem in self._ask_price_map.iteritems():
      yield elem

  def iter_bid(self):
    for price, elem in self._bid_price_map.iteritems():
      yield elem

  def get_ask_array(self, n):
    ask_array = []
    for i, (_, level) in enumerate(self.iteritems_ask()):
      if i > n:
        break
      ask_array.append((level.price, level.qty))
    return ask_array

  def get_bid_array(self, n):
    bid_array = []
    for i, (_, level) in enumerate(self.iteritems_bid()):
      if i > n:
        break
      bid_array.append((level.price, level.qty))
    return bid_array

  def iteritems_ask(self):
    return self._ask_price_map.iteritems()

  def iteritems_bid(self):
    return self._bid_price_map.iteritems()

  def depth_ask(self):
    return len(self._ask_price_map.btree)

  def depth_bid(self):
    return len(self._bid_price_map.btree)

  def get_accumulated_bid_qty(self, from_price, to_price):
    entries = self._bid_price_map.get_entries(from_price, to_price)
    qty = 0
    for entry in entries:
      qty += entry.qty
    return qty

  def iter_bid_book(self, num_level, step_price, min_unit=1.e-8):
    for level in range(num_level):
      from_price = self.bid0().price - level * step_price
      to_price = self.bid0().price - (level + 1) * step_price + min_unit
      yield (from_price, to_price, self.get_accumulated_bid_qty(from_price, to_price))

  def get_accumulated_ask_qty(self, from_price, to_price):
    entries = self._ask_price_map.get_entries(from_price, to_price)
    qty = 0
    for entry in entries:
      qty += entry.qty
    return qty

  def iter_ask_book(self, num_level, step_price, min_unit=1.e-8):
    for level in range(num_level):
      from_price = self.ask0().price + level * step_price
      to_price = self.ask0().price + (level + 1) * step_price - min_unit
      yield (from_price, to_price, self.get_accumulated_ask_qty(from_price, to_price))

  def get_notional_bids_by_qty(self, qty):
    return self._bid_price_map.get_notional_entries_by_qty(qty)

  def get_notional_asks_by_qty(self, qty):
    return self._ask_price_map.get_notional_entries_by_qty(qty)

  def get_notional_bids_by_amt(self, amt):
    return self._bid_price_map.get_notional_entries_by_amt(amt)

  def get_notional_asks_by_amt(self, amt):
    return self._ask_price_map.get_notional_entries_by_amt(amt)

  def get_bid_price_by_amount(self, amt):
    result = self.get_notional_bids_by_amt(amt)
    if amt - result.accumulated > 1e-8:
      raise ThinBookError()
    return result.entries[-1].price

  def get_ask_price_by_amount(self, amt):
    result = self.get_notional_asks_by_amt(amt)
    if amt - result.accumulated > 1e-8:
      raise ThinBookError()
    return result.entries[-1].price

  def get_bid_price_by_qty(self, qty):
    result = self.get_notional_bids_by_qty(qty)
    if qty - result.accumulated > 1e-8:
      raise ThinBookError()
    return result.entries[-1].price

  def get_ask_price_by_qty(self, qty):
    result = self.get_notional_asks_by_qty(qty)
    if qty - result.accumulated > 1e-8:
      raise ThinBookError()
    return result.entries[-1].price


class _AskPriceMapObserver(PriceMapObserverBase):
  def __init__(self, book, observer):
    self._book = book
    self._observer = observer

  def on_clear(self):
    self._observer.on_ask_clear(self._book)

  def on_update(self, price, entry):
    self._observer.on_ask_update(self._book, price, entry)


class _BidPriceMapObserver(PriceMapObserverBase):
  def __init__(self, book, observer):
    self._book = book
    self._observer = observer

  def on_clear(self):
    self._observer.on_bid_clear(self._book)

  def on_update(self, price, entry):
    self._observer.on_bid_update(self._book, price, entry)
